In this example we start with a simple domain model and migrate it just a bit to be a bit more realistic. Along the way we end up using several feature of JPA not yet covered by the previous tutorials.
The Problem
We want to implement a simple system to track books and eventually other resources for a library. This page covers the requirements for Version 1. The problem as stated is bigger than what this tutorial implements. Parts are left as exercises that are scheduled into a course or advanced exercises for students who finish their work early.
Checking out a Book
Description
A Patron checks out one or more books, all of which are due 14 days later.
Basic Course of Events
This use case begins when a Patron identifies him or herself by entering their Patron ID.
The system verifies that the Patron exists and is in good standing.
The system asks the Patron to enter the first resource.
The patron provides the book identifier (each book has a unique identifier because the ISBN is not enough, we need to know WHICH version of Catcher int the Rye the Patron is checking out)
The system records the book, calculates the due date and records the book as on loan to the Patron.
The use case ends.
Alternatives
Num
Step
Description
1
2
The Patron is not in good standing, they have overdue books. Do not allow them to check out any other books until they return all overdue books
2
2
The Patron is not in good standing, they have fines from overdue books they now paid. Do not allow them to check out any books until they pay their fines.
3
2
The Patron is not found (maybe their account was removed due to inactivity). Do not allow the to check out any books
4
5
The Book is not found, log the book and tell the Patron to ask for assistance with the particular book.
5
5
The Book is a reserve book and cannot be checked out. Inform the Patron.
6
5
The due date falls on a day when the Library is not open, move the return date to the next date the library is open.
Returning a Book
Description
A Patron returns a book. The library computes any fines and removes the recording of the loan of the book.
Basic Course of Events
This use case begins when a Patron returns a book. The Patron identifies the book by providing the book identifier.
The system retrieves the loan information for the book.
The system updates the book as being returned and indicates success to the Patron.
The patron indicates they are finished returning books.
The system reports the total of any fines for the books returned as well as any pending fines.
The system asks the user if they would like to pay their fines.
The user indicates the do: Initiate: Paying Fines
The use case ends.
Alternatives
Num
Step
Description
1
2
The book is not on loan, inform the user.
2
3
The book is late, calculate a fine and inform the user of a fine.
3
5
The user owes no fines, the use case ends.
4
6
The user indicates they do not want to pay fines right now. The system informs them they will not be able to checkout books until all fines are paid and the use case ends.
Adding a Book
Description
A Librarian wishes to add a new book into the system.
Basic Course of Events
The Librarian indicates they want to add a new Book.
The system asks the Librarian to provide book information (Book Title, Authors, ISBN, replacement cost)
The Librarian provides the book information.
The system validates the information.
The system creates a new book and assigns the book a unique identifier.
The system indicates the unique identifier for the book (and prints a book label)
The use case ends.
Alternatives
No alternatives listed for this use case.
Removing a Book
Description
The Librarian wishes to take a book out of the system and make it no longer available for checkout.
Basic Course of Events
The Librarian indicates they want to remove a book.
The system asks the librarian for the book identifier.
The Librarian provides the book identifier.
The system validates the book identifier and book.
The system removes the book from the system and indicates success.
Alternatives
Num
Step
Description
1
3
The book identifier is not found. Indicate the error. The use case ends.
2
4
The book is on loan. Remove the loan (ignoring any fines). Indicate the error to the user but complete the use case normally.
3
4
This is the last book with the that particular ISBN. Ask the user to confirm the removal. If confirmed, complete the use case normally. If not confirmed, do not remove the book. Either way, the use case ends.
Adding a Patron
Description
A Librarian adds a new Patron into the system.
Basic Course of Events
The Librarian indicates they wish to add a new Patron to the system.
The system asks the Librarian to provide the Name (first, mi, last), Address (street 1, street 2, city, state, zip), and Phone number(area code + 7 digits).
The system verifies the minimal information is provided.
The system creates a new Patron and assigned a Patron ID.
The system provides the new Patron ID back to the Librarian (and prints a card).
The use case ends.
Alternatives
Num
Step
Description
1
3
Some required information is missing. Indicate the required information and ask the Librarian to perform it. Continue back at step 2.
Removing a Patron
Description
The Librarian wants to remove a Patron.
Basic Course of Events
The Librarian indicates they want to remove a Patron.
The system asks for the Patron's id.
The Librarian provides the id.
The system validates the id.
The system removes the Patron from the system.
Alternatives
Num
Step
Description
1
3
The id is not found. Indicate the error to the user and continue at step 2
2
3
The Patron has outstanding fines. Indicate this to the Librarian and ask to confirm the removal. If confirmed, remove and complete the use case normally. If not confirmed, end the use case without removing the Patron.
3
3
The Patron has outstanding loans. Indicate this to the Librarian and do not allow removal.
Paying Fines
Description
A Patron wishes to pay fines. Note that this use case can be invoked by itself or called from other use cases.
Basic Course of Events
A Patron is identified and their fines calculated.
The system asks for the amount tendered.
The system determines the difference and indicates the difference to the user.
The use case ends.
Alternatives
Num
Step
Description
1
1
The identified Patron has no fines. Indicate this to the user and the use case ends.
2
4
If there is still a balance, the system asks if it should ask for additional reimbursements. If yes, they go back to step 2, otherwise the use case ends.
Record a Book as Unrecoverable
Description
A book is not going to be returned/recovered. Add a fine if the book is on loan.
Basic Course of Events
This use case begins when a book is going to indicated as not returnable.
The system asks the user to provide the book id and a reason.
The user provides the id and reason.
The system retrieves the book.
The system calculates the replacement cost assigns it to the Patron who has it checked out.
The book is removed from the system.
Alternatives
Num
Step
Description
1
3
The book id is not known. Retrieve a list of books checked out to a Patron, select from the list and continue to step 3.
2
3
The book id is not known. Provide the isbn. Select the user who has the book checked out and select the book by id. Continue at step 3.
3
3
The book id is not known. Provide the title. Select the user who has the book checked out and select the book by id. Continue at step 3.
4
5
The book is not checked out, do not calculate any fines.
Reviewing all Checkouts for a Patron
Description
Report all of the books currently checked out by a Patron. Provide the title, isbn and due date. Sort by due date, with the book due the soonest at the beginning. If the user has an outstanding balance, indicate that as well.
Reviewing all Fines for all Patrons
Present a list of all the patrons with fines. Sort by the last name followed by the first name. Provide the name of the user, their phone number and their total balance.
V1 Project Setup
For the rest of this section, when you see <project>, replace it with JpaTutorial3
Create Java Project
Next we need to create a Java project. We'll keep the source separate from the bin directory:
Pull down the File menu and select New:Project
Select Java Project and click on Next
Enter a project name: <project>, again read this to know why I did not use a space in the project name.
Make sure "Create new project in workspace" is selected.
Make sure the JRE selected is 1.5.x or higher. If such a JRE does not show in the list, you can add it through Window->Preferences->JAVA->Installed JRE's.
Select Create separate source and output folders
Click Finish
Create folders and packages
Expand your <project> folder
Select the src directory
Right-click, select new:Folder
Use the name META-INF
Make sure <project> is still selected, right-click and select New:Source Folder
Enter test and click OK
Add Required Libraries
We now need to add two libraries. Note that these steps assume you've already worked through the first tutorial and are working in the same workspace. If you, you'll need to create user libraries. Review Creating User Libraries.
Edit the project properties. Select your <project> and either press alt-enter or right-click and select properties.
Select Java Build Path
Click on the Libraries tab
Click on Add Library
Select User Libraries and click Next
Select JPA_JSE by click on the check box
Click OK
Click on Add Library again
Click on JUnit
Click Next
In the pull-down list, select JUnit 4
Click Finish
Click OK
If you'd like some background information on JUnit, please go here.
Configure Persistence Unit
We now need to create the Persistent Unit definition. We are going to create a file called persistence.xml in the src/META-INF directory with the following contents:
Find the src/META-INF directory (if one does not exist, right-click, select New:Folder, enter META-INF and press enter)
Right click the src/META-INF, select new:File.
Enter persistence.xml for the name and press "OK" (Note: all lowercase. It won't make a difference on Windows XP but it will on Unix.)
Copy the contents (above) into the file and save it
Update persistence.xml
Update Persistence Unit Name
The name of the persistence unit in your just-copied persistence.xml is examplePersistenceUnit in this example we use lis for Library Information System. Make the following change:
A Little Context
Before we get started, this tutorial is deliberately organized in an inconvenient fashion. Why? My target reader is someone in a class I'm teaching (the material is open-source but still targeted). I do not want the student to be able to quickly just copy the whole thing and get it to work without having to put forth some effort. In a classroom situation, I have all of the source code available if I need to help a student get up to speed.
We'll start with a stripped down version of the requirements. This first test suite handles the following test cases:
Create a Patron
Remove a Patron
Update a Patron
Attempt to find Patron that does not exist.
Notice that this suite of tests is for Creating, Reading, Updating and Deleting (CRUD) Patrons.
Assuming you've done Tutorial 2, much of the boilerplate code is going to look the same. First let's write a unit test for each of these test cases: Create a Patron
@Test
publicvoid createAPatron(){final Patron p = createAPatronImpl();final Patron found = dao.retrieve(p.getId());
assertNotNull(found);}private Patron createAPatronImpl(){final Address a = new Address("5080 Spectrum Drive", "Suite 700 West",
"Addison", "TX", "75001");return dao.createPatron("Brett", "Schuchert", "972-555-1212", a);}
This test first creates a patron using a private utility method. This method exists because it is used later in other unit tests.
Looking at the test, it uses an attribute called dao. This is a Data Access Object (which we'll later convert to a stateless Session Bean). This Data Access Object will be responsible for retrieving, creating and removing Patrons. Remove a Patron
@Test
publicvoid removeAPatron(){final Patron p = createAPatronImpl();
dao.removePatron(p.getId());final Patron found = dao.retrieve(p.getId());
assertNull(found);}
This test uses the utility method to create a patron. It then removes it and makes sure that when we try to retrieve it that the Patron no longer exists.
Update a Patron
@Test
publicvoid updateAPatron(){final Patron p = createAPatronImpl();finalString originalPhoneNumber = p.getPhoneNumber();
p.setPhoneNumber(NEW_PN);
dao.update(p);final Patron found = dao.retrieve(p.getId());
assertNotNull(found);
assertFalse(NEW_PN.equals(originalPhoneNumber));
assertEquals(NEW_PN, p.getPhoneNumber());}
We create a patron then update it.
Attempt to find Patron that does not exist
@Test
publicvoid tryToFindPatronThatDoesNotExist(){finalLong id = -18128129831298l;final Patron p = dao.retrieve(id);
assertNull(p);}
Verify that when we try to find a patron that's not found, we get back null.
Supporting Code
We have several veterans returning from previous tutorials. And here they are:
PatronDaoTest
First the imports and the attributes. Note that this is a complete class that will compile. It just doesn't do anything yet.
Initialization of the Logger
This is our 1-time initialization of the logging system.
@BeforeClass
publicstaticvoid initLogger(){// Produce minimal output.
BasicConfigurator.configure();// Comment this line to see a lot of initialization// status logging.Logger.getLogger("org").setLevel(Level.ERROR);}
Getting EMF and EM
Now before each unit test we'll look up the entity manager factory, create a dao, create an entity manager and pass it into a DAO and finally start a transaction.
@Before
publicvoid initEmfAndEm(){
emf = Persistence.createEntityManagerFactory("lis");
dao = new PatronDao();
dao.setEm(emf.createEntityManager());
dao.getEm().getTransaction().begin();}
Clean up after each test
After each test we'll rollback the transaction we created in the pre-test initialization. We'll then close both the entity manager and entity manager factory. This keeps our tests isolated.
We need to create entities. These entities are a bit more well-specified that what you've seen in the previous tutorials. In most cases I believe the extra information is intuitive. Where it is not, I'll try to point out what is going on.
Address.java
packageentity;importjavax.persistence.Column;importjavax.persistence.Entity;importjavax.persistence.GeneratedValue;importjavax.persistence.Id;
@Entity// The next class is a JPA entitypublicclass Address {
@Id // The next attribute (in this case) or method is a key field
@GeneratedValue // the key is auto-generatedprivateLong id;
@Column(length = 50)// the next column is 50 characters in sizeprivateString streetAddress1;
@Column(length = 50)privateString streetAddress2;
@Column(length = 20)privateString city;
@Column(length = 2)privateString state;
@Column(length = 9)privateString zip;public Address(){}public Address(finalString sa1, finalString sa2, finalString city,
finalString state, finalString zip){
setStreetAddress1(sa1);
setStreetAddress2(sa2);
setCity(city);
setState(state);
setZip(zip);}publicString getCity(){return city;}publicvoid setCity(finalString city){this.city = city;}publicString getState(){return state;}publicvoid setState(finalString state){this.state = state;}publicString getStreetAddress1(){return streetAddress1;}publicvoid setStreetAddress1(finalString streetAddress1){this.streetAddress1 = streetAddress1;}publicString getStreetAddress2(){return streetAddress2;}publicvoid setStreetAddress2(finalString streetAddress2){this.streetAddress2 = streetAddress2;}publicString getZip(){return zip;}publicvoid setZip(finalString zip){this.zip = zip;}publicLong getId(){return id;}publicvoid setId(Long id){this.id = id;}}
Patron.java
packageentity;importjavax.persistence.CascadeType;importjavax.persistence.Column;importjavax.persistence.Entity;importjavax.persistence.GeneratedValue;importjavax.persistence.Id;importjavax.persistence.OneToOne;
@Entity// the next class is a JPA entitypublicclass Patron {
@Id
@GeneratedValue
privateLong id;// when in the database, this field cannot be null, as an object in memory// it can
@Column(length = 20, nullable = false)privateString firstName;
@Column(length = 30, nullable = false)privateString lastName;
@Column(length = 11, nullable = false)privateString phoneNumber;// This next field refers to an object that is stored in another table.// All updates are cascaded. So if you persist me, my address, which is in// another table, will be persisted automatically. Updates and removes are// also cascaded automatically.
@OneToOne(cascade = CascadeType.ALL)private Address address;public Patron(finalString fName, finalString lName, finalString phone,
final Address a){
setFirstName(fName);
setLastName(lName);
setPhoneNumber(phone);
setAddress(a);}public Address getAddress(){return address;}publicvoid setAddress(Address address){this.address = address;}publicString getFirstName(){return firstName;}publicvoid setFirstName(String firstName){this.firstName = firstName;}publicLong getId(){return id;}publicvoid setId(Long id){this.id = id;}publicString getLastName(){return lastName;}publicvoid setLastName(String lastName){this.lastName = lastName;}publicString getPhoneNumber(){return phoneNumber;}publicvoid setPhoneNumber(String phoneNumber){this.phoneNumber = phoneNumber;}}
Finally, the Data Access Object
The DAO has the following four methods:
createPatron
retrieve
removePatron
update
We'll look at each of these, although none of this will be new if you've looked at the first tutorial. createPatron
Given the information to create a patron, instantiate one and then persiste it. Note that this is written in a style that will natually fit into a Session Bean.
public Patron createPatron(finalString fname, finalString lname,
finalString phoneNumber, final Address a){final Patron p = new Patron(fname, lname, phoneNumber, a);
getEm().persist(p);return p;}
retrieve
This uses the find method built into the entity manager. It returns null if not found. It first sees if the object is in the first-level cache. If not, it retrieves it from the database.
public Patron retrieve(finalLong id){return getEm().find(Patron.class, id);}
removePatron
To remove an object we have to find it first. You do not provide a class and a key. So we first retrieve it (it might already be in the cache so this may not involve a database hit. We then issue the remove of the object.
publicvoid removePatron(finalLong id){final Patron p = retrieve(id);if(p != null){
getEm().remove(p);}}
update
Update uses the merge method to get its work done. Note that it returns what is returned from merge. Why? The provided patron could be detached (retrieve during another transaction or from a different instance of an entity manager. If this is the case, then the object will not be put into the cache (and therefore become managed). Instead, a new instance is created and the contents of the paramter is copied into the new, managed instance. That new managed instance is returned. If the provided patron is managed, then there's actually not need to even call this method because any changes made to the patron will be reflected in the patron after the transaction is closed.
public Patron update(final Patron p){return getEm().merge(p);}
The rest of the class
Here are the imports, attributes and getters/setters.
JPA Tutorial 3 - A Mini Application PI
In this example we start with a simple domain model and migrate it just a bit to be a bit more realistic. Along the way we end up using several feature of JPA not yet covered by the previous tutorials.
The Problem
We want to implement a simple system to track books and eventually other resources for a library. This page covers the requirements for Version 1. The problem as stated is bigger than what this tutorial implements. Parts are left as exercises that are scheduled into a course or advanced exercises for students who finish their work early.Checking out a Book
DescriptionA Patron checks out one or more books, all of which are due 14 days later.
Basic Course of Events
Alternatives
Returning a Book
DescriptionA Patron returns a book. The library computes any fines and removes the recording of the loan of the book.
Basic Course of Events
Alternatives
Adding a Book
DescriptionA Librarian wishes to add a new book into the system.
Basic Course of Events
Alternatives
No alternatives listed for this use case.
Removing a Book
DescriptionThe Librarian wishes to take a book out of the system and make it no longer available for checkout.
Basic Course of Events
Alternatives
Adding a Patron
DescriptionA Librarian adds a new Patron into the system.
Basic Course of Events
Alternatives
Removing a Patron
DescriptionThe Librarian wants to remove a Patron.
Basic Course of Events
Alternatives
Paying Fines
DescriptionA Patron wishes to pay fines. Note that this use case can be invoked by itself or called from other use cases.
Basic Course of Events
Alternatives
Record a Book as Unrecoverable
DescriptionA book is not going to be returned/recovered. Add a fine if the book is on loan.
Basic Course of Events
Alternatives
Reviewing all Checkouts for a Patron
DescriptionReport all of the books currently checked out by a Patron. Provide the title, isbn and due date. Sort by due date, with the book due the soonest at the beginning. If the user has an outstanding balance, indicate that as well.
Reviewing all Fines for all Patrons
Present a list of all the patrons with fines. Sort by the last name followed by the first name. Provide the name of the user, their phone number and their total balance.V1 Project Setup
For the rest of this section, when you see <project>, replace it with JpaTutorial3Create Java Project
Next we need to create a Java project. We'll keep the source separate from the bin directory:Create folders and packages
Add Required Libraries
We now need to add two libraries. Note that these steps assume you've already worked through the first tutorial and are working in the same workspace. If you, you'll need to create user libraries. Review Creating User Libraries.If you'd like some background information on JUnit, please go here.
Configure Persistence Unit
We now need to create the Persistent Unit definition. We are going to create a file called persistence.xml in the src/META-INF directory with the following contents:persistence.xml
The Steps
Update persistence.xml
Update Persistence Unit NameThe name of the persistence unit in your just-copied persistence.xml is examplePersistenceUnit in this example we use lis for Library Information System. Make the following change:
Your project is now set up.
V1 First Test Suite
The Unit Tests
A Little ContextBefore we get started, this tutorial is deliberately organized in an inconvenient fashion. Why? My target reader is someone in a class I'm teaching (the material is open-source but still targeted). I do not want the student to be able to quickly just copy the whole thing and get it to work without having to put forth some effort. In a classroom situation, I have all of the source code available if I need to help a student get up to speed.
We'll start with a stripped down version of the requirements. This first test suite handles the following test cases:
Notice that this suite of tests is for Creating, Reading, Updating and Deleting (CRUD) Patrons.
Assuming you've done Tutorial 2, much of the boilerplate code is going to look the same. First let's write a unit test for each of these test cases:
Create a Patron
@Test public void createAPatron() { final Patron p = createAPatronImpl(); final Patron found = dao.retrieve(p.getId()); assertNotNull(found); } private Patron createAPatronImpl() { final Address a = new Address("5080 Spectrum Drive", "Suite 700 West", "Addison", "TX", "75001"); return dao.createPatron("Brett", "Schuchert", "972-555-1212", a); }This test first creates a patron using a private utility method. This method exists because it is used later in other unit tests.
Looking at the test, it uses an attribute called dao. This is a Data Access Object (which we'll later convert to a stateless Session Bean). This Data Access Object will be responsible for retrieving, creating and removing Patrons.
Remove a Patron
@Test public void removeAPatron() { final Patron p = createAPatronImpl(); dao.removePatron(p.getId()); final Patron found = dao.retrieve(p.getId()); assertNull(found); }This test uses the utility method to create a patron. It then removes it and makes sure that when we try to retrieve it that the Patron no longer exists.
Update a Patron
@Test public void updateAPatron() { final Patron p = createAPatronImpl(); final String originalPhoneNumber = p.getPhoneNumber(); p.setPhoneNumber(NEW_PN); dao.update(p); final Patron found = dao.retrieve(p.getId()); assertNotNull(found); assertFalse(NEW_PN.equals(originalPhoneNumber)); assertEquals(NEW_PN, p.getPhoneNumber()); }We create a patron then update it.Attempt to find Patron that does not exist
@Test public void tryToFindPatronThatDoesNotExist() { final Long id = -18128129831298l; final Patron p = dao.retrieve(id); assertNull(p); }Verify that when we try to find a patron that's not found, we get back null.
Supporting Code
We have several veterans returning from previous tutorials. And here they are:PatronDaoTest
First the imports and the attributes. Note that this is a complete class that will compile. It just doesn't do anything yet.
Initialization of the Logger
This is our 1-time initialization of the logging system.
@BeforeClass public static void initLogger() { // Produce minimal output. BasicConfigurator.configure(); // Comment this line to see a lot of initialization // status logging. Logger.getLogger("org").setLevel(Level.ERROR); }Getting EMF and EM
Now before each unit test we'll look up the entity manager factory, create a dao, create an entity manager and pass it into a DAO and finally start a transaction.
@Before public void initEmfAndEm() { emf = Persistence.createEntityManagerFactory("lis"); dao = new PatronDao(); dao.setEm(emf.createEntityManager()); dao.getEm().getTransaction().begin(); }Clean up after each test
After each test we'll rollback the transaction we created in the pre-test initialization. We'll then close both the entity manager and entity manager factory. This keeps our tests isolated.
@After public void closeEmAndEmf() { dao.getEm().getTransaction().rollback(); dao.getEm().close(); emf.close(); }The Entities
We need to create entities. These entities are a bit more well-specified that what you've seen in the previous tutorials. In most cases I believe the extra information is intuitive. Where it is not, I'll try to point out what is going on.Address.java
Patron.java
Finally, the Data Access Object
The DAO has the following four methods:
We'll look at each of these, although none of this will be new if you've looked at the first tutorial.
createPatron
Given the information to create a patron, instantiate one and then persiste it. Note that this is written in a style that will natually fit into a Session Bean.
retrieve
This uses the find method built into the entity manager. It returns null if not found. It first sees if the object is in the first-level cache. If not, it retrieves it from the database.
removePatron
To remove an object we have to find it first. You do not provide a class and a key. So we first retrieve it (it might already be in the cache so this may not involve a database hit. We then issue the remove of the object.
update
Update uses the merge method to get its work done. Note that it returns what is returned from merge. Why? The provided patron could be detached (retrieve during another transaction or from a different instance of an entity manager. If this is the case, then the object will not be put into the cache (and therefore become managed). Instead, a new instance is created and the contents of the paramter is copied into the new, managed instance. That new managed instance is returned. If the provided patron is managed, then there's actually not need to even call this method because any changes made to the patron will be reflected in the patron after the transaction is closed.
The rest of the class
Here are the imports, attributes and getters/setters.
<-Back